use thiserror::Error;

use crate::ssa::{self, AssignmentTarget, VarRef};
use crate::tysys::{ArgRef, FieldRef, FnContext};

#[derive(Debug, Error)]
pub enum Error {
    #[error("var {0:?} ref before assignment")]
    VarRefBeforeAssign(VarRef),

    #[error("arg {0:?} out of bounds")]
    ArgOob(ArgRef),

    #[error("field {0:?} out of bounds")]
    FieldOob(FieldRef),

    #[error("buffer length unknown in this context")]
    UnknownBufLen,

    #[error("buffer is immutable")]
    BufferImmutable,
}

/// Checks ref integrity within a scope.
pub fn check_ref_integrity(
    block: &ssa::Block,
    scope: &mut Vec<VarRef>,
    ctx: &impl FnContext,
) -> Result<(), Error> {
    // Does this need to be its own function?
    check_vri_block(block, scope, ctx)
}

/// Checks that vars have been assigned to in the scopes they're used in.
fn check_vri_block(
    block: &ssa::Block,
    scope: &mut Vec<VarRef>,
    ctx: &impl FnContext,
) -> Result<(), Error> {
    let start_len = scope.len();
    check_vri_block_inner(block, scope, ctx)?;
    assert!(scope.len() >= start_len);
    scope.truncate(start_len);
    Ok(())
}

fn check_vri_block_inner(
    block: &ssa::Block,
    scope: &mut Vec<VarRef>,
    ctx: &impl FnContext,
) -> Result<(), Error> {
    for ent in block.entries() {
        match ent {
            ssa::SsaEntry::Assignment(vr, expr) => {
                check_vri_expr(expr, scope, ctx)?;

                // If we're assigning to a local, add it to the list.
                if let AssignmentTarget::Local(local) = vr {
                    if !check_scope_contains_local(scope, local, ctx) {
                        scope.push(*local);
                    }
                }
            }

            ssa::SsaEntry::AbortIf(arg, _) => {
                check_scope_contains_arg(scope, arg, ctx)?;
            }

            ssa::SsaEntry::WriteBuf(buf, off, val) => {
                check_scope_contains_assign_target(scope, buf, ctx)?;
                check_scope_contains_arg(scope, off, ctx)?;
                check_scope_contains_arg(scope, val, ctx)?;
            }

            ssa::SsaEntry::Return => {}

            ssa::SsaEntry::ReturnValue(arg) => {
                check_scope_contains_arg(scope, arg, ctx)?;
            }
        }
    }

    // Also have to check the return value.
    if let Some(rv) = block.result() {
        check_scope_contains_arg(scope, rv, ctx)?;
    }

    Ok(())
}

fn check_scope_contains_assign_target(
    scope: &[VarRef],
    target: &AssignmentTarget,
    ctx: &impl FnContext,
) -> Result<(), Error> {
    match target {
        AssignmentTarget::Field(field) => {
            if ctx.get_instance_field_type(*field).is_none() {
                return Err(Error::FieldOob(*field));
            }
        }
        AssignmentTarget::Local(local) => {
            // FIXME weird bungling to make this code play nice
            check_scope_contains_arg(scope, &ssa::SsaArg::Local(*local), ctx)?;
        }
    }

    Ok(())
}

fn check_vri_expr(
    expr: &ssa::SsaExpr,
    scope: &mut Vec<VarRef>,
    ctx: &impl FnContext,
) -> Result<(), Error> {
    match expr {
        ssa::SsaExpr::Copy(arg) => check_scope_contains_arg(scope, arg, ctx)?,
        ssa::SsaExpr::Monop(_, arg) => check_scope_contains_arg(scope, arg, ctx)?,
        ssa::SsaExpr::Binop(_, lhs, rhs) => {
            check_scope_contains_arg(scope, lhs, ctx)?;
            check_scope_contains_arg(scope, rhs, ctx)?;
        }
        ssa::SsaExpr::Cond(cond, iftrueblock, iffalseblock) => {
            check_scope_contains_arg(scope, cond, ctx)?;
            check_vri_block(iftrueblock, scope, ctx)?;
            check_vri_block(iffalseblock, scope, ctx)?;
        }
        ssa::SsaExpr::Ternary(cond, iftrue, iffalse) => {
            check_scope_contains_arg(scope, cond, ctx)?;
            check_scope_contains_arg(scope, iftrue, ctx)?;
            check_scope_contains_arg(scope, iffalse, ctx)?;
        }
        ssa::SsaExpr::Cast(_, arg) => {
            check_scope_contains_arg(scope, arg, ctx)?;
        }
        ssa::SsaExpr::BufferLen(arg) => {
            check_scope_contains_arg(scope, arg, ctx)?;

            // FIXME this doesn't make sense here anymore
            /*if !ctx.have_buffer_len() {
                return Err(Error::UnknownBufLen);
            }*/
        }
        ssa::SsaExpr::ReadInt(_, buf, off) => {
            check_scope_contains_arg(scope, buf, ctx)?;
            check_scope_contains_arg(scope, off, ctx)?;
        }
        ssa::SsaExpr::ReadBuf(buf, off, len) => {
            check_scope_contains_arg(scope, buf, ctx)?;
            check_scope_contains_arg(scope, off, ctx)?;
            check_scope_contains_arg(scope, len, ctx)?;
        }

        _ => {}
    }

    Ok(())
}

fn check_scope_contains_arg(
    scope: &[VarRef],
    arg: &ssa::SsaArg,
    ctx: &impl FnContext,
) -> Result<(), Error> {
    match arg {
        ssa::SsaArg::Literal(_) => {}
        ssa::SsaArg::Local(vr) => {
            if !check_scope_contains_local(scope, vr, ctx) {
                return Err(Error::VarRefBeforeAssign(*vr));
            }
        }
        ssa::SsaArg::Field(field) => {
            if ctx.get_instance_field_type(*field).is_none() {
                return Err(Error::FieldOob(*field));
            }
        }
        ssa::SsaArg::Arg(idx) => {
            if ctx.get_arg_type(*idx).is_none() {
                return Err(Error::ArgOob(*idx));
            }
        }
    }

    Ok(())
}

fn check_scope_contains_local(scope: &[VarRef], vr: &VarRef, _ctx: &impl FnContext) -> bool {
    // Switch to rfind or something?
    scope.contains(&vr)
}

/// Checks if the block mutates the underlying buffer or container.  Does not
/// check that all variables are real.  If a code path contains a write that is
/// unreachable we still count it as mutating.
pub fn check_block_mutates_buf(block: &ssa::Block) -> Result<bool, Error> {
    for ent in block.entries() {
        match ent {
            // The obvious case.
            ssa::SsaEntry::WriteBuf(_, _, _) => return Ok(true),

            // Some assignment branches contain blocks that may also have
            // assignments in them.
            ssa::SsaEntry::Assignment(_, expr) => match expr {
                ssa::SsaExpr::Cond(_, iftrue, iffalse) => {
                    if check_block_mutates_buf(iftrue)? || check_block_mutates_buf(iffalse)? {
                        return Ok(true);
                    }
                }
                _ => {}
            },

            _ => {}
        }
    }

    Ok(false)
}
